package testing

import (
	"context"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/docker/cli/cli/connhelper"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/filters"
	"github.com/docker/docker/client"
	"github.com/docker/go-connections/nat"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"gorm.io/gorm/schema"

	"mxshop_srvs/shared/config"
)

const (
	image         = "mysql:8"
	containerPort = "3306/tcp"
)

var mysqlURI string

// DockerClientFromSSH Obtain the Docker client through SSH
func DockerClientFromSSH(ctx context.Context, daemonURL string) (*client.Client, error) {
	// 使用golang的Docker SDK,通过ssh远程访问docker守护进程
	helper, err := connhelper.GetConnectionHelper(daemonURL)
	if err != nil {
		return nil, err
	}
	httpClient := &http.Client{
		Transport: &http.Transport{
			DialContext: helper.Dialer,
		},
	}
	cli, err := client.NewClientWithOpts(client.FromEnv,
		client.WithHTTPClient(httpClient),
		client.WithHost(helper.Host),
		client.WithDialContext(helper.Dialer),
	)
	if err != nil {
		return nil, err
	}
	cli.NegotiateAPIVersion(ctx) // 该函数可以协商API版本并解决版本不匹配问题
	return cli, nil
}

// RunWithMysqlInDocker runs the tests with
// a mysqldb instance in a docker container.
func RunWithMysqlInDocker(m *testing.M, databaseName string) int {
	var err error
	var cli *client.Client
	var hostIP, bindingHostIP string

	ctx := context.Background()

	// 根据规则去控制,连接本地的docker还是远程的docker
	switch urlSli := strings.SplitN(config.DaemonUrl, "://", 2); urlSli[0] {
	case "ssh":
		u, err := url.Parse(config.DaemonUrl)
		if err != nil {
			panic(err)
		}
		hostIP = u.Hostname()
		bindingHostIP = "0.0.0.0"
		cli, err = DockerClientFromSSH(context.Background(), config.DaemonUrl)
		if err != nil {
			panic(err)
		}
	default:
		hostIP = "127.0.0.1"
		bindingHostIP = hostIP

		cli, err = client.NewClientWithOpts(client.FromEnv)
		if err != nil {
			panic(err)
		}
	}

	// 根据镜像名,查询已存在的镜像
	filter := filters.NewArgs(filters.Arg("reference", image))
	images, err := cli.ImageList(context.Background(), types.ImageListOptions{Filters: filter})
	if err != nil {
		panic(err)
	}

	// 如果没有这个镜像,则去pull
	if len(images) == 0 {
		// ImagePull 请求 docker 主机从远程注册表中提取镜像。
		// 如果操作未经授权，它会执行特权功能并再试一次。由调用者来处理 io.ReadCloser 并正确关闭它。
		out, err := cli.ImagePull(context.Background(), image, types.ImagePullOptions{})
		if err != nil {
			panic(err)
		}
		defer func(out io.ReadCloser) {
			_ = out.Close()
		}(out)
		_, _ = io.Copy(os.Stdout, out)
	}

	// 创建容器
	resp, err := cli.ContainerCreate(ctx, &container.Config{
		// 容器的配置数据。只保存容器的可移植配置信息。
		// “可移植”意味着“独立于我们运行的主机”(容器内部的配置)。
		Image: image, // 对应docker镜像的名字
		ExposedPorts: nat.PortSet{ // 内部要暴露端口列表(比如mysql:3306,redis:6379)
			containerPort: {},
		},

		// 配置环境变量
		Env: []string{
			"MYSQL_ALLOW_EMPTY_PASSWORD=true",
			// "MYSQL_ROOT_PASSWORD=123456",
			"TZ=Asia/Shanghai",
			fmt.Sprintf("MYSQL_DATABASE=%s", databaseName),
		},

		// cmd
		Cmd: []string{
			"--character-set-server=utf8mb4",
			"--collation-server=utf8mb4_general_ci",
			"--default-authentication-plugin=mysql_native_password",
			"--explicit_defaults_for_timestamp=true",
			"--lower_case_table_names=1",
		},
	},
		// 主机的配置。保存不可移植的配置信息。
		&container.HostConfig{
			// 暴露端口(容器)和主机之间的端口映射
			PortBindings: nat.PortMap{
				containerPort: []nat.PortBinding{
					{
						HostIP:   bindingHostIP, // 主机IP地址
						HostPort: "0",           // 主机端口号。0代表随机端口
					},
				},
			},
		}, nil, nil, "")
	if err != nil {
		panic(err)
	}
	containerId := resp.ID

	// ContainerStart 根据containerID,向 docker 守护进程发送请求以启动容器。
	err = cli.ContainerStart(ctx, containerId, types.ContainerStartOptions{})
	if err != nil {
		panic(err)
	}

	// ContainerInspect 返回容器信息。
	insRes, err := cli.ContainerInspect(ctx, resp.ID)
	if err != nil {
		panic(err)
	}

	defer func() {
		// ContainerRemove 根据containerID,杀死并从 docker 主机中删除一个容器。
		err = cli.ContainerRemove(ctx, containerId, types.ContainerRemoveOptions{
			Force: true,
		})
		if err != nil {
			panic(err)
		}

		// 循环删除容器里所有的挂载
		for _, res := range insRes.Mounts {
			err := cli.VolumeRemove(ctx, res.Name, true)
			if err != nil {
				panic(err)
			}
		}
	}()

	// Ports是PortBinding的集合
	hostPort := insRes.NetworkSettings.Ports[containerPort][0]
	mysqlURI = fmt.Sprintf("root:@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
		hostIP, hostPort.HostPort, databaseName)

	return m.Run()
}

// NewLogger creates a gorm logger interface.
func NewLogger() logger.Interface {
	return logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
		logger.Config{
			SlowThreshold: time.Second,   // 慢 SQL 阈值
			LogLevel:      logger.Silent, // Log level
			Colorful:      true,          // 禁用彩色打印
		},
	)
}

type GormOption interface {
	apply(*gorm.Config)
}

func defaultOption() gorm.Config {
	return gorm.Config{
		NamingStrategy: schema.NamingStrategy{
			SingularTable: true,
		},
		Logger: NewLogger(),
	}
}

type funcOption struct {
	f func(*gorm.Config)
}

func (fdo *funcOption) apply(do *gorm.Config) {
	fdo.f(do)
}

func newFuncOption(f func(*gorm.Config)) *funcOption {
	return &funcOption{
		f: f,
	}
}

func WithNowFunc(f func() time.Time) GormOption {
	return newFuncOption(func(g *gorm.Config) {
		g.NowFunc = f
	})
}

// NewClient creates a client connected to the mysql instance in docker.
func NewClient(opts ...GormOption) (*gorm.DB, error) {
	// 这里再做一个保护
	if mysqlURI == "" {
		return nil, fmt.Errorf("mysql uri not set. Please run RunWithMysqlInDocker in TestMain")
	}

	defaultOpt := defaultOption()
	for _, opt := range opts {
		opt.apply(&defaultOpt)
	}

	// 防止docker启mysql太慢,做的重试机制
	times := 10
	var db *gorm.DB
	var err error
	for i := 0; i < times; i++ {
		db, err = gorm.Open(mysql.Open(mysqlURI), &defaultOpt)
		if err == nil {
			break
		}
		time.Sleep(time.Duration(i*500) * time.Millisecond)
	}
	return db, err
}

// NewDefaultClient creates a client connected to defaultMysqlDNS
func NewDefaultClient(databaseName string, opts ...GormOption) (*gorm.DB, error) {
	defaultOpt := defaultOption()
	for _, opt := range opts {
		opt.apply(&defaultOpt)
	}

	return gorm.Open(mysql.Open(fmt.Sprintf(config.DefaultMysqlDNS, databaseName)), &defaultOpt)
}
